/**
 *
 */
package jp.seraph.jsmf.codec;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
import java.util.ArrayList;
import java.util.List;

import net.sf.json.JSONArray;
import net.sf.json.JSONObject;

import jp.seraph.jsade.math.PolynomialFunction;
import jp.seraph.jsade.model.AngleVelocityCalculator;
import jp.seraph.jsade.model.JointIdentifier;
import jp.seraph.jsade.model.JointIdentifierContainer;
import jp.seraph.jsade.model.PolynomialAngleVelocityCalculator;
import jp.seraph.jsmf.message.MessageUtil;
import jp.seraph.jsmf.motion.BlankMotionSession;
import jp.seraph.jsmf.motion.DefaultMotion;
import jp.seraph.jsmf.motion.DefaultMotionSession;
import jp.seraph.jsmf.motion.Motion;
import jp.seraph.jsmf.motion.MotionElement;
import jp.seraph.jsmf.motion.MotionSession;
import jp.seraph.jspf.pose.PoseManager;

/**
 * PoseNameLinkMotionElementを使用したMotionと文字列を相互変換するMotionCodec実装です。
 *
 */
public class PoseNameLinkMotionCodec implements MotionCodec {
    public PoseNameLinkMotionCodec(PoseManager aPoseManager, JointIdentifierContainer aIdContainer){
        mPoseManager = aPoseManager;
        mIdContainer = aIdContainer;
    }

    private PoseManager mPoseManager;
    private JointIdentifierContainer mIdContainer;

    private static final String VERSION = "Version";
    private static final String MOTION = "MOTION";
    private static final String SESSION = "Session";
    private static final String SESSION_KIND = "ElementKind";
    private static final String SESSION_NAME = "ElementName";
    private static final String BLANK_STEP = "BlankStep";
    private static final String ELEMENT = "Element";
    private static final String ELEMENT_NAME = "ElementName";
    private static final String CONDITION_ELEMENT_NAME = "ConditionElementName";
    private static final String PERCENTAGE = "Percentage";
    private static final String POSE = "Pose";
    private static final String POSE_NAME = "PoseName";
    private static final String JOINTS = "Joints";
    private static final String CALCULATOR = "Calclator";
    private static final String SCALE_FACTOR = "ScaleFactor";
    private static final String FUNCTION = "Function";
    private static final String TERMS = "Terms";
    private static final String COEFFICIENT = "Coefficient";
    private static final String EXPOTENT = "Expotent";

    private final String ENCODER_VERSION = "0.1.1";
    private final String BLANK_SESSION = "Blank";
    private final String NORMAL_SESSION = "Normal";

    /**
     *
     * @see jp.seraph.jsmf.codec.MotionDecoder#decode(java.io.Reader)
     */
    public Motion decode(Reader aReader) throws IOException {
        JSONObject tObject = JSONObject.fromObject(readToEnd(aReader));

        String tVersion = tObject.getString(VERSION);
        if(!ENCODER_VERSION.equals(tVersion))
            throw new IllegalArgumentException(MessageUtil.noSupportMotionVersion(tVersion));

        return decodeMotion(tObject.getJSONObject(MOTION));
    }

    private String readToEnd(Reader aReader) throws IOException {
        BufferedReader tReader = new BufferedReader(aReader);
        StringBuilder tBuilder = new StringBuilder();
        String tLine;
        while ((tLine = tReader.readLine()) != null) {
            tBuilder.append(tLine);
            tBuilder.append(System.getProperty("line.separator"));
        }

        return tBuilder.toString();
    }

    private Motion decodeMotion(JSONObject aObject){
        DefaultMotion tResult = new DefaultMotion();
        for (MotionSession tSession : decodeSessions(aObject.getJSONArray(SESSION))) {
            tResult.addSession(tSession);
        }

        return tResult;

    }

    private List<MotionSession> decodeSessions(JSONArray aArray){
        List<MotionSession> tResult = new ArrayList<MotionSession>();

        int tCount = aArray.size();
        for(int i=0; i<tCount; i++)
            tResult.add(decodeSession(aArray.getJSONObject(i)));

        return tResult;
    }

    private MotionSession decodeSession(JSONObject aObject){
        String tSessionKind = aObject.getString(SESSION_KIND);
        if(BLANK_SESSION.equals(tSessionKind))
            return decodeBlankSession(aObject);

        String tSessionName = aObject.getString(SESSION_NAME);

        DefaultMotionSession tResult = new DefaultMotionSession(tSessionName);

        JSONArray tElements = aObject.getJSONArray(ELEMENT);
        for (MotionElement tElement : decodeElements(tElements)) {
            tResult.addElement(tElement);
        }

        return tResult;
    }

    private MotionSession decodeBlankSession(JSONObject aObject){
        return new BlankMotionSession(aObject.getInt(BLANK_STEP));
    }

    private List<MotionElement> decodeElements(JSONArray aArray){
        List<MotionElement> tResult = new ArrayList<MotionElement>();
        int tCount = aArray.size();
        for(int i=0; i<tCount; i++)
            tResult.add(decodeElement(aArray.getJSONObject(i)));

        return tResult;
    }

    private MotionElement decodeElement(JSONObject aObject){
        String tElementName;
        String tConditionElementName;
        String tPoseName;
        AngleVelocityCalculator tCalculator;
        List<JointIdentifier> tIds;
        int tPercentage;

        JSONObject tPose = aObject.getJSONObject(POSE);
        tPoseName = tPose.getString(POSE_NAME);
        tIds = decodeJoints(tPose.getJSONArray(JOINTS));

        tElementName = aObject.getString(ELEMENT_NAME);
        tConditionElementName = aObject.getString(CONDITION_ELEMENT_NAME);
        tPercentage = aObject.getInt(PERCENTAGE);
        tCalculator = decodeCalculator(aObject.getJSONObject(CALCULATOR));


        PoseNameLinkMotionElementData tData = new PoseNameLinkMotionElementData(tElementName, tPoseName, tConditionElementName, tPercentage, tCalculator, tIds.toArray(new JointIdentifier[0]));
        PoseNameLinkMotionElement tElement = new PoseNameLinkMotionElement(mPoseManager, tData);

        return tElement;
    }

    private List<JointIdentifier> decodeJoints(JSONArray aArray){
        List<JointIdentifier> tResult = new ArrayList<JointIdentifier>();
        int tCount = aArray.size();
        for(int i=0; i<tCount; i++)
            tResult.add(mIdContainer.getIdentifier(aArray.getString(i)));

        return tResult;
    }

    private AngleVelocityCalculator decodeCalculator(JSONObject aObject){
        double tScaleFactor;
        PolynomialFunction tFunction;

        tScaleFactor = aObject.getDouble(SCALE_FACTOR);
        tFunction = decodeFunction(aObject.getJSONObject(FUNCTION));

        PolynomialAngleVelocityCalculator tResult = new PolynomialAngleVelocityCalculator(tScaleFactor, tFunction);

        return tResult;
    }

    private PolynomialFunction decodeFunction(JSONObject aObject){
        PolynomialFunction tResult = new PolynomialFunction(decodeTerms(aObject.getJSONArray(TERMS)).toArray(new PolynomialFunction.TermEntry[0]));
        return tResult;
    }

    private List<PolynomialFunction.TermEntry> decodeTerms(JSONArray aArray){
        List<PolynomialFunction.TermEntry> tResult = new ArrayList<PolynomialFunction.TermEntry>();

        int tCount = aArray.size();
        for(int i=0; i<tCount; i++)
            tResult.add(decodeTerm(aArray.getJSONObject(i)));

        return tResult;
    }

    private PolynomialFunction.TermEntry decodeTerm(JSONObject aObject){
        double tCoefficient = aObject.getDouble(COEFFICIENT);
        double tExpotent = aObject.getDouble(EXPOTENT);

        PolynomialFunction.TermEntry tResult = new PolynomialFunction.TermEntry(tCoefficient, tExpotent);

        return tResult;
    }

    /**
     *
     * @see jp.seraph.jsmf.codec.MotionEncoder#encode(jp.seraph.jsmf.motion.Motion, java.io.Writer)
     */
    public void encode(Motion aMotion, Writer aWriter) throws IOException {
        JSONObject tObject = new JSONObject();
        tObject.put(VERSION, ENCODER_VERSION);
        tObject.put(MOTION, encode(aMotion));
        aWriter.write(tObject.toString());
    }

    private JSONObject encode(Motion aMotion){
        JSONObject tResult = new JSONObject();
        tResult.put(SESSION, encodeSessions(aMotion.toSessionList()));

        return tResult;
    }

    private JSONArray encodeSessions(List<MotionSession> aSessions){
        JSONArray tResult = new JSONArray();

        List<MotionSession> tSessionList = aSessions;
        for (MotionSession tMotionSession : tSessionList) {
            tResult.add(encode(tMotionSession));
        }

        return tResult;
    }

    private JSONObject encode(MotionSession aSession){
        if(aSession instanceof BlankMotionSession)
            return encode((BlankMotionSession)aSession);

        JSONObject tResult = new JSONObject();

        tResult.put(SESSION_KIND, NORMAL_SESSION);
        tResult.put(SESSION_NAME, aSession.getName());
        tResult.put(ELEMENT, encodeElements(aSession.toElementList()));

        return tResult;
    }

    private JSONObject encode(BlankMotionSession aBlankSession){
        JSONObject tResult = new JSONObject();
        tResult.put(SESSION_KIND, BLANK_SESSION);
        tResult.put(BLANK_STEP, aBlankSession.getStep());

        return tResult;
    }

    private JSONArray encodeElements(List<MotionElement> aElements){
        JSONArray tResult = new JSONArray();
        for (MotionElement tElement : aElements) {
            tResult.add(encode(tElement));
        }

        return tResult;
    }

    private JSONObject encode(MotionElement aElement){
        if(!(aElement instanceof PoseNameLinkMotionElement))
            throw new IllegalArgumentException(MessageUtil.noSupportMotionElement(aElement.getClass().getName()));

        return encode((PoseNameLinkMotionElement)aElement);
    }

    private JSONObject encode(PoseNameLinkMotionElement aElement){
        PoseNameLinkMotionElementData tData = aElement.getElementData();

        JSONObject tResult = new JSONObject();
        tResult.put(ELEMENT_NAME, tData.getName());
        tResult.put(CONDITION_ELEMENT_NAME, tData.getConditionElementName());
        tResult.put(PERCENTAGE, tData.getStartPercentage());
        tResult.put(POSE, encode(tData.getPoseName(), tData.getIdentifiers()));
        tResult.put(CALCULATOR, encode(tData.getCalculator()));

        return tResult;
    }

    private JSONObject encode(String aPoseName, List<JointIdentifier> aJoints){
        JSONObject tResult = new JSONObject();
        tResult.put(POSE_NAME, aPoseName);
        tResult.put(JOINTS, encode(aJoints));

        return tResult;
    }

    private JSONArray encode(List<JointIdentifier> aJoints){
        JSONArray tResult = new JSONArray();
        for (JointIdentifier tId : aJoints) {
            tResult.add(tId.toJointName());
        }

        return tResult;
    }

    private JSONObject encode(AngleVelocityCalculator aCalculator){
        if(!(aCalculator instanceof PolynomialAngleVelocityCalculator))
            throw new IllegalArgumentException(MessageUtil.noSupportCalculator(aCalculator.getClass().getName()));

        return encode((PolynomialAngleVelocityCalculator)aCalculator);
    }

    private JSONObject encode(PolynomialAngleVelocityCalculator aCalculator){
        JSONObject tResult = new JSONObject();
        tResult.put(SCALE_FACTOR, aCalculator.getScaleFactor());
        tResult.put(FUNCTION, encode(aCalculator.getFunction()));

        return tResult;
    }

    private JSONObject encode(PolynomialFunction aFunction){
        JSONObject tResult = new JSONObject();
        tResult.put(TERMS, encode(aFunction.getEntries()));

        return tResult;
    }

    private JSONArray encode(PolynomialFunction.TermEntry[] aEntories){
        JSONArray tResult = new JSONArray();

        for (PolynomialFunction.TermEntry tEntory : aEntories) {
            tResult.add(encode(tEntory));
        }

        return tResult;
    }

    private JSONObject encode(PolynomialFunction.TermEntry aEntory){
        JSONObject tResult = new JSONObject();
        tResult.put(COEFFICIENT, aEntory.getCoefficient());
        tResult.put(EXPOTENT, aEntory.getExponent());

        return tResult;
    }
}
